﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Linq;
using System.Web.Hosting;
using System.Web.Security;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Builders;

namespace mvcForum.MembershipProviderTest.Security {

	public class MongoDBRoleProvider : RoleProvider {
		private MongoConnection conn;

		private MongoConnection Connection {
			get {
				if (this.conn == null) {
					this.conn = MongoConnection.Create("User.MongoDB");
				}
				return this.conn;
			}
		}

		/// <summary>
		/// Gets or sets the name of the application to store and retrieve role information for.
		/// </summary>
		public override String ApplicationName { get; set; }

		public override void AddUsersToRoles(String[] usernames, String[] roleNames) {
			if (usernames == null) {
				throw new ArgumentNullException("usernames");
			}
			if (roleNames == null) {
				throw new ArgumentNullException("roleNames");
			}
			foreach (String roleName in roleNames) {
				if (!this.RoleExists(roleName)) {
					throw new ProviderException(String.Format("The role '{0}' was not found.", roleName));
				}
			}

			foreach (String username in usernames) {
				MembershipUser membershipUser = Membership.GetUser(username);
				if (membershipUser == null) {
					throw new ProviderException(String.Format("The user '{0}' was not found.", username));
				}

				User user = this.Connection.GetCollection<User>().FindOneAs<User>(this.BuildUserQuery(new IMongoQuery[] {
								Query.EQ(User.FieldNames.UpperUsername, username.ToUpperInvariant())
							}));
				foreach (String roleName in roleNames) {
					if (this.IsUserInRole(username, roleName)) {
						throw new ProviderException(String.Format("The user '{0}' is already in role '{1}'.", username, roleName));
					}

					user.Roles.Add(roleName);
				}
				this.Connection.GetCollection<User>().Save<User>(user);
			}
		}

		/// <summary>
		/// Adds a new role to the data source for the configured applicationName.
		/// </summary>
		/// <param name="roleName">The name of the role to create.</param>
		public override void CreateRole(String roleName) {
			// Do we already have a role with that name?
			if (this.RoleExists(roleName)) {
				// Yes, fail!
				throw new ProviderException(String.Format("The role '{0}' already exists.", roleName));
			}

			// No, let's create one then!
			this.Connection.GetCollection<Role>().Insert<Role>(new Role {
				ApplicationName = this.ApplicationName,
				Name = roleName
			});
		}

		/// <summary>
		/// Removes a role from the data source of the configured applicationName.
		/// </summary>
		/// <param name="roleName">The name of the role to delete.</param>
		/// <param name="throwOnPopulatedRole">If true, throw an exception if roleName has one or more members and do not delete roleName.</param>
		/// <returns>True if the roleName was deleted.</returns>
		public override Boolean DeleteRole(String roleName, Boolean throwOnPopulatedRole) {
			// Does the role exist at all?
			if (!this.RoleExists(roleName)) {
				throw new ProviderException(String.Format("The role '{0}' was not found.", roleName));
			}

			// Any users in the given role?
			String[] users = this.GetUsersInRole(roleName);
			if (throwOnPopulatedRole && users.Any()) {
				throw new ProviderException("This role cannot be deleted because there are users present in it.");
			}

			// Let's delete any user-role relations!
			this.RemoveUsersFromRoles(users, new String[] { roleName });
			// Let's delete the role itself.
			return this.Connection
				.GetCollection<Role>()
				.Remove(this.BuildQuery(Query.EQ(Role.FieldNames.Name, roleName))).Ok;
		}

		private IMongoQuery BuildQuery(params IMongoQuery[] query) {
			return Query.And(new IMongoQuery[] { this.BuildApplicationQuery() }.Union(query).ToArray());
		}

		private IMongoQuery BuildApplicationQuery() {
			return Query.EQ(User.FieldNames.ApplicationName, this.ApplicationName);
		}

		private IMongoQuery BuildUserQuery(params IMongoQuery[] query) {
			return Query.And(new IMongoQuery[] { this.BuildUserApplicationQuery() }.Union(query).ToArray());
		}

		private IMongoQuery BuildUserApplicationQuery() {
			return Query.EQ(User.FieldNames.ApplicationName, Membership.ApplicationName);
		}

		public override String[] FindUsersInRole(String roleName, String usernameToMatch) {
			if (!this.RoleExists(roleName)) {
				throw new ArgumentException("roleName");
			}
			if (String.IsNullOrWhiteSpace(usernameToMatch)) {
				throw new ArgumentNullException("usernameToMatch");
			}

			return this.Connection
						.GetCollection<User>()
						.FindAs<User>(this.BuildUserQuery(new IMongoQuery[] {
								Query.Matches(User.FieldNames.UpperUsername, usernameToMatch.ToUpperInvariant()),
								Query.In(User.FieldNames.Roles, new BsonArray(new String[] { roleName }))
							})
						)
						.Select(u => u.Username)
						.ToArray();
		}

		public override String[] GetAllRoles() {
			return this.Connection
						.GetCollection<Role>()
						.FindAs<Role>(Query.EQ(Role.FieldNames.ApplicationName, this.ApplicationName))
						.Select(r => r.Name)
						.ToArray();
		}

		public override String[] GetRolesForUser(String username) {
			if (String.IsNullOrWhiteSpace(username)) {
				throw new ArgumentNullException("username");
			}
			MembershipUser membershipUser = Membership.GetUser(username);
			if (membershipUser == null) {
				throw new ProviderException(String.Format("The user '{0}' was not found.", username));
			}

			//Membership.ApplicationName
			User user = this.Connection
							.GetCollection<User>()
							.FindOneAs<User>(
								this.BuildUserQuery(
									new IMongoQuery[] {
										Query.EQ(User.FieldNames.UpperUsername, username.ToUpperInvariant())
									}
								)
							);
			return user.Roles.ToArray();
		}

		public override String[] GetUsersInRole(String roleName) {
			if (!this.RoleExists(roleName)) {
				throw new ArgumentException("roleName");
			}
			return this.Connection.GetCollection<User>()
						.FindAs<User>(this.BuildUserQuery(new IMongoQuery[] {
								Query.In("roles", new BsonArray(new String[] { roleName }))
							})
						)
						.Select(u => u.Username)
						.ToArray();
		}

		/// <summary>
		/// Initiales the provider.
		/// </summary>
		/// <param name="name">The friendly name of the provider.</param>
		/// <param name="config">A collection of the name/value pairs representing the provider-specific attributes specified in the cofiguration for this provider.</param>
		public override void Initialize(String name, NameValueCollection config) {
			this.ApplicationName = config["applicationName"] ?? HostingEnvironment.ApplicationVirtualPath ?? String.Empty;
			base.Initialize(name, config);
		}

		public override Boolean IsUserInRole(String username, String roleName) {
			if (!this.RoleExists(roleName)) {
				throw new ProviderException(String.Format("The role '{0}' was not found.", roleName));
			}
			if (String.IsNullOrWhiteSpace(username)) {
				throw new ArgumentNullException("username");
			}
			MembershipUser user = Membership.Provider.GetUser(username, false);
			if (user == null) {
				throw new ProviderException(String.Format("The user '{0}' was not found.", username));
			}

			return this.Connection.GetCollection<User>().FindOne(this.BuildUserQuery(new IMongoQuery[] {
						Query.EQ(User.FieldNames.UpperUsername, username.ToUpperInvariant()),
						Query.EQ(User.FieldNames.Roles, roleName)
					})
				) != null;
		}

		/// <summary>
		/// Removes the specified user names from the specified roles for the configured applicationName.
		/// </summary>
		/// <param name="usernames">A string array of user names to be removed from the specified roles.</param>
		/// <param name="roleNames">A string array of role names to remove the specified user names from.</param>
		public override void RemoveUsersFromRoles(String[] usernames, String[] roleNames) {
			if (usernames == null) {
				throw new ArgumentNullException("usernames");
			}
			if (roleNames == null) {
				throw new ArgumentNullException("roleNames");
			}
			if (usernames.Any(String.IsNullOrWhiteSpace)) {
				throw new ArgumentException("usernames");
			}
			if (roleNames.Any(String.IsNullOrWhiteSpace)) {
				throw new ArgumentException("roleNames");
			}
			BsonArray users = new BsonArray(usernames.Select(u => u.ToUpperInvariant()));
			BsonArray roles = new BsonArray(roleNames);

			List<User> userList = this.Connection.GetCollection<User>().FindAs<User>(
					this.BuildUserQuery(new IMongoQuery[] { Query.In(User.FieldNames.UpperUsername, users) })
				).ToList();
			if (usernames.Length != userList.Count) {
				throw new ArgumentException("one or more users does not exist!");
			}

			//if (roleNames.Length != this.Context.GetCollection<Role>().Count(Query.And(Query.EQ(RoleNames.ApplicationName, this.ApplicationName), Query.In(RoleNames.RoleName, roles)))) {
			//    throw new ArgumentException("one or more roles does not exist!");
			//}

			var userRepo = this.Connection.GetCollection<User>();
			foreach (User user in userList) {
				foreach (String role in roleNames) {
					if (user.Roles.Contains(role)) {
						user.Roles.Remove(role);
					}
				}
				userRepo.Save<User>(user);
			}
		}

		/// <summary>
		/// Gets a value indicating whether the specified role name already exists in the data source for the configured applicationName.
		/// </summary>
		/// <param name="roleName">The name of the role to search for in the data source.</param>
		/// <returns></returns>
		public override Boolean RoleExists(String roleName) {
			if (String.IsNullOrWhiteSpace(roleName)) {
				throw new ArgumentNullException("roleName");
			}
			return this.Connection
						.GetCollection<Role>()
						.FindOneAs<Role>(
							this.BuildQuery(
								new IMongoQuery[] {
									Query.EQ(Role.FieldNames.Name, roleName)
							})
						) != null;
		}
	}
}
